package de.fuberlin.enginetesting;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import de.fuberlin.enginetesting.Algorithms.AlgName;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources.Theme;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.BatteryManager;
import android.telephony.TelephonyManager;

public class Engine {

	//Constants
	//Right now we assume we only have 1 server (in the future we could search the nearest server)
	public static final String SERVER_URL = "http://www.mi.fu-berlin.de/offload/run";
	public static final String PREFS_NAME = "OffloadingEnginePrefs";
	public static final long SERVER_INST_MS = 6666666; //Calculated through practical values (javap -c), although we know the server has 4 cores of 2.5GHz

	private Context appContext; //The context of the main Activity of the Android application using this engine
	private int pingCounter; //Needed to calculate the ping
	private float numberOfSpeedRelUpdates;	//Needed for the constant updating of the speed relation of the Engine
	private long lastCalculatedCost;	//Needed for updating the speed relation TODO
	private long timePingStart; //Needed to calculate the ping
	private long[] pingsArray; //Needed to calculate the ping
	private long ping; //Represents the time to query and get an answer from the server (actually not done with a real ping command over ICMP) 
	private boolean connAvailable;
	private boolean serverAvailable;
	private String connType; //Not needed, just to display info
		
	//The next 7 variables are not needed. Just to display info.
	private String realServerTime; //It will have the time in milliseconds that the server needed to execute an algorithm (in case of offloading)
	private double androidEstimatedRuntime;
	private double offloadingEstimatedTime;
	private double serverEstimatedRuntime;
	private boolean doOffloading;
	private long overallTime;
	
	//Persistent values, will be loaded (calculated only in the first execution of the Android application using this engine)
	private float relationServerAndroid;

	public Engine(Context theContext) {
		appContext = theContext;
		//We initialize some variables with default values although they will be properly obtained soon (just in case there is an early potentially offloadable algorithm)
		ping = 100;
		connAvailable = true;
		serverAvailable = true;
		connType = "Unknown";
		getConnectionDetails();
		loadEngine();
		if (connAvailable) averagePing();
	}

	//Getters, not needed, just to display info
	public long getPing() {
		return ping;		
	}

	public float getRelation() {
		return relationServerAndroid;		
	}
	
	public String getConnType() {
		return connType;
	}
	
	public boolean isConnectionAvailable() {
		return connAvailable;
	}
	
	public boolean isServerAvailable() {
		return serverAvailable;
	}
	
	public String getRealServerTime() {
		return realServerTime;
	}
	
	public double getAndroidEstimatedRuntime() {
		return androidEstimatedRuntime;
	}
	
	public double getOffloadingEstimatedTime() {
		return offloadingEstimatedTime;
	}
	
	public boolean isOffloadingDone() {
		return doOffloading;
	}
	
	public long getOverallTime() {
		return overallTime;		
	}
	
	public double getServerEstimatedRuntime() {
		return serverEstimatedRuntime;
	}
	//End getters

	private void getConnectionDetails() {
		ConnectivityManager connMgr = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
		NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
		if (networkInfo != null && networkInfo.isConnected()) {
			connAvailable = true;
			int netType = networkInfo.getType();
			int netSubtype = networkInfo.getSubtype();
			if (netType == ConnectivityManager.TYPE_WIFI) {
				connType = "Wi-Fi";
			}
			else {
				if(netType == ConnectivityManager.TYPE_MOBILE  && netSubtype == TelephonyManager.NETWORK_TYPE_UMTS) connType = "3G";
				else connType = "Other";
			}	
		}
		else {
			connAvailable = false;
			serverAvailable = false;
		}		
	}
	
	/*
	 * Decide where to execute the algorithm, locally (no offloading) or on the server (offloading is done).
	 * Returns true if the decision is to offload, false otherwise.
	 */
	private boolean decide(AlgName algName, String... parameters) {
		
		//The next 3 variables are used and updated only with the purpose of displaying information (some of them are redundant)
		androidEstimatedRuntime = -1;
		offloadingEstimatedTime = -1;
		serverEstimatedRuntime = -1;
		lastCalculatedCost = -1;
		
		getConnectionDetails(); //Refresh connection info. If connection is available, does not check if server is available (it would take time).
		
		if (connAvailable && serverAvailable) {
			
			if (relationServerAndroid == -1) 
				{
				relationServerAndroid = calculateRelation();
				System.out.println("the first relation was obtained and is" + relationServerAndroid);
				}
			
			lastCalculatedCost = Algorithms.getCost(algName, parameters);
			double estServerTime = lastCalculatedCost / ((double) SERVER_INST_MS); //Estimated server execution time
			double estAndroidTime = estServerTime * relationServerAndroid; //Estimated Android execution time
			double pingTime = ping;
						
			androidEstimatedRuntime = estAndroidTime;
			offloadingEstimatedTime = pingTime + estServerTime;
			serverEstimatedRuntime = estServerTime;
						
			//Time saving criteria
			if (pingTime + estServerTime < estAndroidTime) return true;
			else return false;
				
		}
		else return false;
	}

	/* 
	 * Decide where to execute the algorithm, locally (no offloading) or on the server (offloading is done), executes it and retrieves the result.
	 * Returns the result on success (in String form), or the String "Error" on failure.
	 */
	public String execute(AlgName algName, String... parameters) {
		realServerTime = "None";
		overallTime = -1;
		doOffloading = decide(algName, parameters);
		long startTime = System.currentTimeMillis();
		String algResult = "";
		if (doOffloading) { //Do offloading
			String urlParams = Engine.urlEncodeParams(parameters);
			String queryString = Engine.SERVER_URL + "?algName=" + algName.toString() + urlParams;
			GetServerData getServerData = new GetServerData();
			getServerData.execute(queryString);
			try {
				algResult = getServerData.get(); //This can also return the word "Error"
			} catch (Exception e) {
				e.printStackTrace();
				serverAvailable = false;
				algResult = "Error";
			}
			if (algResult.equals("Error")) { //Unable to retrieve the data. URL may be invalid or the server may be down.
				serverAvailable = false;
			}
			else {
				try {
					realServerTime = getElementValueFromXML(algResult, "runtime");
					algResult = getElementValueFromXML(algResult, "result");
				} catch (Exception e) {
					e.printStackTrace();
					algResult = "Error";
				}
			}
		}
		else { //Do not offload, execute locally in the Android mobile device
			algResult = Algorithms.executeLocally(algName, parameters);
		}
		overallTime = System.currentTimeMillis() - startTime;
		if(!doOffloading && overallTime > 15 && lastCalculatedCost != -1) updateSpeedRelation(overallTime);//TODO
		return algResult;
	}

	private void updateSpeedRelation(long newTime) {
		// TODO Auto-generated method stub
		if (numberOfSpeedRelUpdates<20) numberOfSpeedRelUpdates++;
		System.out.println("The old speed Relation was " + relationServerAndroid + "The updated times is" + numberOfSpeedRelUpdates);
		long newAndroidInstMs = lastCalculatedCost/newTime;
		float newRelation = (float) (((double) Engine.SERVER_INST_MS) / ((double) newAndroidInstMs));
		System.out.println("The new speed Relation is " + newRelation);
		relationServerAndroid=relationServerAndroid*((numberOfSpeedRelUpdates-1)/numberOfSpeedRelUpdates) 
								+ newRelation*(1/numberOfSpeedRelUpdates);
		System.out.println("The overall new speed Relation is " + relationServerAndroid);
	}
	
	public void savePreferencesData(){
		System.out.println("The data will be saved");
		SharedPreferences sPrefs = appContext.getSharedPreferences(Engine.PREFS_NAME, Context.MODE_PRIVATE);
		SharedPreferences.Editor editor = sPrefs.edit();
		editor.putFloat("SpeedRelUpdates", numberOfSpeedRelUpdates);
		editor.putFloat("deviceServerRelation",relationServerAndroid);
		editor.commit();
	}
	
	private void loadEngine(){	//TODO
		SharedPreferences sPrefs = appContext.getSharedPreferences(Engine.PREFS_NAME, Context.MODE_PRIVATE);
		relationServerAndroid = sPrefs.getFloat("deviceServerRelation", -1);
		System.out.println("The loaded relation is" + relationServerAndroid );
		numberOfSpeedRelUpdates = sPrefs.getFloat("SpeedRelUpdates", 1);
		System.out.println("The number of Accurations done has been taken and is "+numberOfSpeedRelUpdates);
	}
	
	// Execute an algorithm in the Android device with a known cost in the server, in order to establish a speed relation (how many times faster is the server)
	private float calculateRelation() {
		long endAlgorithmTime;
		long nLoops = 1000000;
		long startAlgorithmTime = System.currentTimeMillis();
		Algorithms.executeLocally(AlgName.doSomeLoops, Long.valueOf(nLoops).toString());
		endAlgorithmTime = System.currentTimeMillis();
		long executionTime = endAlgorithmTime - startAlgorithmTime;
		long AndroidInstMs = Algorithms.getCost(AlgName.doSomeLoops, Long.valueOf(nLoops).toString())/executionTime;
		return (float) (((double) Engine.SERVER_INST_MS) / ((double) AndroidInstMs));
	}

	private void averagePing() {
		pingCounter = 0;
		pingsArray = new long[10];
		timePingStart = System.currentTimeMillis();
		new GetPing().execute(Engine.SERVER_URL);
	}

	private class GetPing extends AsyncTask<String, Void, Integer> {

		@Override
		protected Integer doInBackground(String... urlAddress) {
			try {
				DefaultHttpClient httpClient = new DefaultHttpClient();
				HttpGet httpGet = new HttpGet(urlAddress[0]);
				ResponseHandler<String> resHandler = new BasicResponseHandler();
				httpClient.execute(httpGet, resHandler);
				return 0;
			} catch (Exception e) {
				e.printStackTrace();
				return -1;
			}
		}

		@Override
		protected void onPostExecute(Integer respCode) {
			if (respCode == -1) { //Ping failed
				serverAvailable = false;
			}
			else {
				long timePingEnd = System.currentTimeMillis();
				pingsArray[pingCounter] = timePingEnd - timePingStart;
				pingCounter++;
				if (pingCounter < 10) {
					timePingStart = System.currentTimeMillis();
					new GetPing().execute(Engine.SERVER_URL);
				}
				else if (pingCounter == 10) {
					ping = Engine.calcAverage(pingsArray);
				}
			}
		}	
	}

	private class GetServerData extends AsyncTask<String, Void, String> {
		@Override
		protected String doInBackground(String... urlAddress) {
			try {
				DefaultHttpClient httpClient = new DefaultHttpClient();
				HttpGet httpGet = new HttpGet(urlAddress[0]);
				ResponseHandler<String> resHandler = new BasicResponseHandler();
				String responseData = httpClient.execute(httpGet, resHandler);
				return responseData;
			} catch (Exception e) {
				e.printStackTrace();
				return "Error";
			}
		}
	}
	
	private static String urlEncodeParams(String... parameters) {
		if (parameters == null) return "";
		String urlParams = "";
		for (int i = 0; i < parameters.length; i++) {
			String urlEncodedParam;
			try {
				urlEncodedParam = URLEncoder.encode(parameters[i], "UTF-8");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
				return "Error";
			}
			urlParams = urlParams + "&param" + (i + 1) + "=" + urlEncodedParam;
		}
		return urlParams;
	}

	private static String getElementValueFromXML(String xmlString, String tagName) 
			throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		DocumentBuilder db = dbf.newDocumentBuilder();
		InputSource is = new InputSource();
		is.setCharacterStream(new StringReader(xmlString));
		Document doc = db.parse(is);
		NodeList summary = doc.getElementsByTagName(tagName);
		Element line = (Element) summary.item(0);
		return getCharacterDataFromElement(line);
	}

	private static String getCharacterDataFromElement(Element e) {
		Node child = e.getFirstChild();
		if (child instanceof CharacterData) {
			CharacterData cd = (CharacterData) child;
			return cd.getData();
		}
		return "";
	}

	private static long calcAverage (long[] valuesArray) {

		//Calculate the average
		long valuesSum = 0;
		for (int i = 0; i < valuesArray.length; i++) {
			valuesSum += valuesArray[i];
		}
		long average = valuesSum/valuesArray.length;

		//Recalculate the average omitting all values with a high deviation
		long niceValuesSum = 0;
		int niceValuesCount = 0;
		for (int i = 0; i < valuesArray.length; i++) {
			long auxValue = valuesArray[i];
			if (auxValue < 0) auxValue *= -1;
			long auxAverage = average;
			if (auxAverage < 0) auxAverage *= -1;
			if (auxValue <= auxAverage * 2) {
				niceValuesSum += valuesArray[i];
				niceValuesCount++;
			}
		}
		return niceValuesSum/niceValuesCount;
	}

}
